using System;
using System.IO;
using MLSharp.Classification;
using MLSharp.Utilities;
using System.Linq;

namespace MLSharp.SupportVectorMachines
{
	/// <summary>
	/// A libSVM classifier.
	/// </summary>
	/// <remarks>
	/// The actual libSVM model is stored on the file system.  This
	/// class uses the command-line version of libSVM is used to 
	/// classify new data using the model.
	/// </remarks>
	public class LibSvmExClassifier : IClassifier, IDisposable
	{
		#region Logger

		/// <summary>
		/// The logger for the type.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// Flag that is set to true when the Dispose method on the instance is invoked.
		/// </summary>
		private bool mDisposed;

		/// <summary>
		/// Flag that controls whether or not the model file should be removed.
		/// </summary>
		private bool mDisposeOfModelFile;

		/// <summary>
		/// The working directory to use for temporary files.
		/// </summary>
		private string mWorkingDirectory;

		/// <summary>
		/// The path to svm-predict.exe.
		/// </summary>
		private string mSvmPredictPath;

		#endregion

		#region Public Properties

		/// <summary>
		/// The full path to the libSVM model file.
		/// </summary>
		public string ModelFile { get; private set; }

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a classifier that will use the specified model file
		/// to classify new instances.  The model file is removed when the
		/// classifier is disposed.
		/// </summary>
		/// <param name="modelFileName">The path to the model file.</param>
		/// <param name="svmPredictPath">The path to svm-predict.exe.</param>
		/// <param name="workingDirectory">The working directory to use.</param>
		public LibSvmExClassifier(string modelFileName, string svmPredictPath, string workingDirectory)
			: this(modelFileName, svmPredictPath, workingDirectory, true)
		{
		}

		/// <summary>
		/// Creates a classifier that will use the specified model file to 
		/// classify new instances. 
		/// </summary>
		/// <param name="modelFileName">The path to the model.</param>
		/// <param name="svmPredictPath">The path to svm-predict.exe.</param>
		/// <param name="workingDirectory">The working directory to use.</param>
		/// <param name="disposeOfModelFile">True to dispose of the model
		/// file when the classifier is disposed, false otherwise.</param>
		public LibSvmExClassifier(string modelFileName, string svmPredictPath, string workingDirectory, bool disposeOfModelFile)
		{
			ModelFile = modelFileName;
			mDisposeOfModelFile = disposeOfModelFile;
			mSvmPredictPath = svmPredictPath;
			mWorkingDirectory = workingDirectory;

			Initialize();
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Prepares the classifier for use.
		/// </summary>
		private void Initialize()
		{
			mWorkingDirectory = Path.GetFullPath(mWorkingDirectory);

			if (!Directory.Exists(mWorkingDirectory))
			{
				Directory.CreateDirectory(mWorkingDirectory);
			}

			if (string.IsNullOrEmpty(mSvmPredictPath))
			{
				throw new InvalidOperationException("No path specified for svm-predict.exe.");
			}

			mSvmPredictPath = Path.GetFullPath(mSvmPredictPath);

			//If the training program can't be found, explode.
			if (!File.Exists(mSvmPredictPath))
			{
				throw new InvalidOperationException("Unable to find libSVM at " + Path.GetFullPath(mSvmPredictPath));
			}

		}

		#endregion

		#region Implementation of IClassifier

		/// <summary>
		/// Classifies all the instances in the specified data set.
		/// </summary>
		/// <param name="dataSet">The data set to classify.</param>
		/// <returns>The results of classifying each instance.</returns>
		public ClassificationResult[] Classify(IDataSet dataSet)
		{
			//Step 1: Wrte the data set to a file that svm-predict can read from.
			string testingFile = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".data");
			LibSvmDataWriter.WriteData(dataSet, testingFile);

			//This is where the output will go
			string outputFile = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".out");

			//Step 2: Invoke svm-predict
			ProcessOutput output = ProcessWrapper.Run(mSvmPredictPath, mWorkingDirectory,
			                                   string.Format("-b 1 \"{0}\" \"{1}\" \"{2}\"", testingFile, Path.GetFullPath(ModelFile), outputFile));

			//Clean up the testing file, we don't need it anymore
			File.Delete(testingFile);

			//Step 3: Make sure the outpu file exists
			if (!File.Exists(outputFile))
			{
				//If not, log the output of libSVM and throw an exception
				if (mLogger.IsErrorEnabled)
				{
					mLogger.ErrorFormat("libSVM did not create an output file.  Dumping libSVM output: \r\n{0}",
										output);
				}

				throw new InvalidOperationException("libSVM did not create an output file.");
			}

			//Step 4: Process the output file and create classification results
			string[] outputLines = File.ReadAllLines(outputFile);

			//Go ahead and remove the output file now, we're finished with it.
			File.Delete(outputFile);

			ClassificationResult[] results = new ClassificationResult[dataSet.Instances.Count];

			//There should be exactly 2 more line than results
			if (results.Length + 1 != outputLines.Length)
			{
				if (mLogger.IsErrorEnabled)
				{
					mLogger.ErrorFormat("Output length does not match expected value.  Dumping output:\r\n{0}",
					                    string.Join("\r\n", outputLines));
				}

				throw new ApplicationException("Unexpected output from svm-predict.");
			}

			//Get the label indexes, and map them back to our target attribute values.
			string[] labelValues =
				outputLines[0].Substring(6).Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);

			string[] classLabels = new string[2];

			//Convert both values to double's so that 1 == 1.0
			if (double.Parse(labelValues[0]) == double.Parse(dataSet.Attributes[dataSet.TargetAttributeIndex].PossibleValues[0]))
			{
				classLabels[0] = dataSet.Attributes[dataSet.TargetAttributeIndex].PossibleValues[0];
				classLabels[1] = dataSet.Attributes[dataSet.TargetAttributeIndex].PossibleValues[1];
			}
			else
			{
				classLabels[0] = dataSet.Attributes[dataSet.TargetAttributeIndex].PossibleValues[1];
				classLabels[1] = dataSet.Attributes[dataSet.TargetAttributeIndex].PossibleValues[0];				
			}

			//Convert the output into result objects
			for (int i=0; i < results.Length; i++)
			{
				//The first line contains the possible labels, so skip it and break up
				//the current result line.
				string[] values = outputLines[i + 1].Split(' ');

				ClassificationResult result = 
					new ClassificationResult(classLabels[Array.IndexOf(labelValues, values[0])],
                                           dataSet.Instances[i].ClassValue)
                  	{
                  		Confidence = values.Skip(1).Select(value => double.Parse(value)).Max()
                  	};

				result.ID = dataSet.Instances[i].Label;

				results[i] = result;
			}

			//Step 5: Return the results
			return results;
		}

		#endregion

		#region IDisposable/Finalizer Stuff

		/// <summary>
		/// Cleans up any temporary files used by the instance.
		/// </summary>
		public void Dispose()
		{
			if (!mDisposed)
			{
				//Remove temporary files
				if (File.Exists(ModelFile) && mDisposeOfModelFile)
				{
					File.Delete(ModelFile);
				}

				mDisposed = true;
			}

			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Destructor, insures that Dispose is called.
		/// </summary>
		~LibSvmExClassifier()
		{
			Dispose();
		}

		#endregion
	}
}
